#! /usr/bin/env python
# -*- coding: utf8 -*-
#
#  gitlab irc sender
#  Copyright (C) 2016-2017  Andrei Karas (4144)
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

import copy
import logging
import logging.handlers
import yaml
from collections import OrderedDict

from code.dictconfig import DictConfig
from code.project import ProjectClass

class Configuration:
    # log file
    log_file = "./hooks.log"
    # maximum log size
    log_max_size = 104857600
    # log level
    log_level = logging.INFO

    # web server name and version
    http_web_server_name = "nginx"

    # gitlab compare commits url
    gitlab_compare_path = "{homepage}/compare/{commit1}...{commit2}"
    # gitlab commit url
    gitlab_commit_path = "{homepage}/commit/{commit}"
    # gitlab commits group or branch
    gitlab_commits_path = "{homepage}/commits/{name}"
    # gitlab build url
    gitlab_build_path = "{homepage}/builds/{build}"
    # gitlab pipeline url
    gitlab_pipeline_path = "{homepage}/pipelines/{pipeline}"
    # commit size in letters in compare gitlab link
    gitlab_compare_commit_size = 6

    # number of letters showed from commit id
    commit_size = 7

    # pause after each irc message
    irc_message_pause = 2
    # pause in gitlab api thread
    api_sleep_time = 3
    # first irc message about push
    irc_push_header_message = "[\x0302{project}\x0f] \x0307{author}\x0f pushed {count} commits \x0303{branch}\x0f {url}"
    # main push commit message, if comment to big, it continue in next line
    irc_push_commit_message = "[\x0302{project}\x0f] \x0307{author}\x0f \x0303{shortid}\x0f - {message}"
    # continue push commit message
    irc_push_commit_message_continue = "{message}"
    # irc message about pushed tag
    irc_push_add_tag_message = "[\x0302{project}\x0f] \x0307{author}\x0f pushed tag \x0303{tag}\x0f on commit \x0303{shortid}\x0f {url}"
    # irc message about removed tag
    irc_push_remove_tag_message = "[\x0302{project}\x0f] \x0307{author}\x0f deleted tag \x0303{tag}\x0f"
    # irc message about created branch
    irc_push_create_branch_message = "[\x0302{project}\x0f] \x0307{author}\x0f created/changed branch \x0303{branch}\x0f on commit \x0303{shortid}\x0f {url}"
    # irc message about deleted branch
    irc_push_delete_branch_message = "[\x0302{project}\x0f] \x0307{author}\x0f deleted branch \x0303{branch}\x0f"
    # irc message about new note
    irc_push_add_note_message = "[\x0302{project}\x0f] \x0307{author}\x0f add note on commit \x0303{shortid}\x0f {url}"
    # irc message about failed build
    irc_build_failed_message = "[\x0302{project}\x0f] Build \x0307#{build} {name}\x0f for commit \x0303{shortid}\x0f from \x0303{branch}\x0f failed {url}"
    # irc message about success build
    irc_build_success_message = "[\x0302{project}\x0f] Build \x0307#{build}\x0f for commit \x0303{shortid}\x0f from \x0303{branch}\x0f success {url}"
    # irc message about failure stage
    irc_stage_failed_message = "[\x0302{project}\x0f] Build \x0307#{build}\x0f for commit \x0303{shortid}\x0f from \x0303{branch}\x0f failed {url}"
    # irc message about success pipeline
    irc_pipeline_success_message = "[\x0302{project}\x0f] Pipeline \x0307#{pipeline}\x0f for commit \x0303{shortid}\x0f from \x0303{branch}\x0f success {url}"
    # irc message about failed pipeline
    irc_pipeline_failed_message = "[\x0302{project}\x0f] Pipeline \x0307#{pipeline}\x0f for commit \x0303{shortid}\x0f from \x0303{branch}\x0f failed {url}"
    # irc message about opened merge request
    irc_merge_request_opened_message = "[\x0302{project}\x0f] \x0307{author}\x0f opened merge request \x0303!{id}\x0f: {title} {url}"
    # irc message about closed merge request
    irc_merge_request_closed_message = "[\x0302{project}\x0f] \x0307{author}\x0f closed merge request \x0303!{id}\x0f: {title} {url}"
    # irc message about reopened merge request
    irc_merge_request_reopened_message = "[\x0302{project}\x0f] \x0307{author}\x0f reopened merge request \x0303!{id}\x0f: {title} {url}"
    # irc message about updated merge request
    irc_merge_request_updated_message = "[\x0302{project}\x0f] \x0307{author}\x0f updated merge request \x0303!{id}\x0f: {title} {url}"
    # irc message about merged merge request
    irc_merge_request_merged_message = "[\x0302{project}\x0f] \x0307{author}\x0f merged merge request \x0303!{id}\x0f: {title} {url}"
    # irc message about note in merge request
    irc_merge_request_add_note_message = "[\x0302{project}\x0f] \x0307{author}\x0f add note into merge request \x0303!{id}\x0f (\x0303{source_namespace}:{source_branch}\x0f to \x0303{branch}\x0f) {url}"
    # irc message about note in issue
    irc_issue_add_note_message = "[\x0302{project}\x0f] \x0307{author}\x0f add note into issue \x0303#{id}\x0f {url}"
    # irc message about note in snippet
    irc_snippet_add_note_message = "[\x0302{project}\x0f] \x0307{author}\x0f add note into snippet \x0303${id}\x0f {url}"
    # irc message about opened issue
    irc_issue_opened_message = "[\x0302{project}\x0f] \x0307{author}\x0f opened issue \x0303#{id}\x0f: {title} {url}"
    # irc message about closed issue
    irc_issue_closed_message = "[\x0302{project}\x0f] \x0307{author}\x0f closed issue \x0303#{id}\x0f: {title} {url}"
    # irc message about reopened issue
    irc_issue_reopened_message = "[\x0302{project}\x0f] \x0307{author}\x0f reopened issue \x0303#{id}\x0f: {title} {url}"
    # irc message about updated issue
    irc_issue_updated_message = "[\x0302{project}\x0f] \x0307{author}\x0f updated issue \x0303#{id}\x0f: {title} {url}"
    web_hook_default_service = "gitlab"

    # codeship messages

    # irc message about failed build
    irc_codeship_build_failed_message = "[\x0302{project}\x0f] Build for commit \x0303{shortid}\x0f from \x0303{branch}\x0f failed {url}"
    # irc message about success build
    irc_codeship_build_success_message = "[\x0302{project}\x0f] Build for commit \x0303{shortid}\x0f from \x0303{branch}\x0f success {url}"

    # github messages

    # irc message about failed build
    irc_github_build_failed_message = "[\x0302{project}\x0f] {description} for commit \x0303{shortid}\x0f from \x0303{branch}\x0f {url}"
    # irc message about success build
    irc_github_build_success_message = "[\x0302{project}\x0f] {description} for commit \x0303{shortid}\x0f from \x0303{branch}\x0f {url}"
    # irc message about opened issue
    irc_github_issues_opened_message = "[\x0302{project}\x0f] \x0307{author}\x0f opened issue \x0303#{id}\x0f: {title} {url}"
    # irc message about edited issue
    irc_github_issues_edited_message = "[\x0302{project}\x0f] \x0307{author}\x0f edited issue \x0303#{id}\x0f: {title} {url}"
    # irc message about closed issue
    irc_github_issues_closed_message = "[\x0302{project}\x0f] \x0307{author}\x0f closed issue \x0303#{id}\x0f: {title} {url}"
    # irc message about reopened issue
    irc_github_issues_reopened_message = "[\x0302{project}\x0f] \x0307{author}\x0f reopened issue \x0303#{id}\x0f: {title} {url}"
    # irc message about created issue comment
    irc_github_issue_comment_created_message = "[\x0302{project}\x0f] \x0307{author}\x0f add comment to issue \x0303#{id}\x0f: {title} {url}"
    # irc message about edited issue comment
    irc_github_issue_comment_edited_message = "[\x0302{project}\x0f] \x0307{author}\x0f edited comment in issue \x0303#{id}\x0f: {title} {url}"
    # irc message about deleted issue comment
    irc_github_issue_comment_deleted_message = "[\x0302{project}\x0f] \x0307{author}\x0f deleted comment from issue \x0303#{id}\x0f: {title} {url}"

    def __init__(self):
        self.global_options = DictConfig()
        self.projects = dict()
        self.show_info = True

    def addDefaultOption(self, name, value):
        if name not in self.global_options:
            self.global_options[name] = value
            setattr(self.global_options, name, value)

    def load_yaml(self, name):
        class OrderedLoader(yaml.SafeLoader):
            pass
        def construct_mapping(loader, node):
            loader.flatten_mapping(node)
            return OrderedDict(loader.construct_pairs(node))
        OrderedLoader.add_constructor(
            yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
            construct_mapping)
        with open(name, "r") as r:
            return yaml.load(r, OrderedLoader)

    def loadDefaultOptions(self):
        self.addDefaultOption("show_commits", True)
        self.addDefaultOption("show_commit_messages", True)
        # number of lines with commit message per commit
        self.addDefaultOption("max_message_lines_per_commit", 3)
        # if build failed, show error message in irc
        self.addDefaultOption("show_build_failures", True)
        # if build success messages for "allowed to fail"
        self.addDefaultOption("show_build_success_for_allowed_to_fail", False)
        # if build failure messages for "allowed to fail"
        self.addDefaultOption("show_build_failures_for_allowed_to_fail", False)
        # if pipeline success, show success message in irc
        self.addDefaultOption("show_pipeline_success", True)
        # if pipeline failed, show error message in irc
        self.addDefaultOption("show_pipeline_failures", True)
        # build stage name what will trigger build success message
        self.addDefaultOption("last_build_stage", "")
        # build stage name what will trigger build failed message
        self.addDefaultOption("last_build_fail_stage", "")
        # custom irc message after build failed message
        self.addDefaultOption("build_fail_custom_message", "")
        # directory where located files with channels created by ii bot
        self.addDefaultOption("ii_server_directory", "irc.freenode.net")
        # listen host / name
        self.addDefaultOption("listen_host", "0.0.0.0")
        # listen port
        self.addDefaultOption("listen_port", 8000)
        # if set to true, return error for any incorrect/not allowed request
        self.addDefaultOption("http_return_error_if_not_supported", True)
        # true, "all", "any" - save all incoming json data into log.
        # false - not save
        # unsupported - only if unsupported
        self.addDefaultOption("http_save_json", "unsupported")
        # if set to True, will follow build_accept_allow_failure attributes
        self.addDefaultOption("build_accept_allow_failure", True)
        # web hook service. Supported values: gitlab, codeship, github
        self.addDefaultOption("service", self.web_hook_default_service)
        # if enabled it reverse commits order in first push due gitlab bug.
        self.addDefaultOption("push_first_push_fix", True)
        # custom irc message after pipeline failed message
        self.addDefaultOption("pipeline_fail_custom_message", "")
        # gitlab api token for access gitlab api
        self.addDefaultOption("gitlab_api_token", "")
        # gitlab api url for access gitlab api
        self.addDefaultOption("gitlab_api_url", "https://gitlab.com/api")
        # gitlab api retry count
        self.addDefaultOption("gitlab_api_retry_count", 3)
        # gitlab build retry count
        self.addDefaultOption("build_retry_count", 3)
        # build retry flag
        self.addDefaultOption("build_retry", "never")
        self.global_options.update_fields()

    def loadProjectConfiguration(self, projectConf, name):
        if self.show_info == True:
            debugChannels = set()
        url = projectConf["url"]
        project = ProjectClass()
        project.name = name
        project.url = url
        actionsDict = dict()
        channelsDict = dict()
        allowBranches = []
        denyBranches = []
        options = copy.copy(self.global_options)
        if "options" in projectConf:
            options.update(projectConf["options"])
            options.update_fields()
        if "channels" in projectConf:
            channels = projectConf["channels"]
            for channelobj in channels:
                channel = channelobj
                channelName = "#" + channel
                channelsDict[channelName] = copy.copy(options)
                if "actions" in channels[channel]:
                    objects = channels[channel]["actions"]
                else:
                    objects = None
                if "options" in channels[channel]:
                    channelsDict[channelName].update(channels[channel]["options"])
                    channelsDict[channelName].update_fields()
                if objects is not None:
                    for obj in objects:
                        if obj not in actionsDict:
                            actionsDict[obj] = []
                        actionsDict[obj].append(channelName)
                if self.show_info == True:
                    debugChannels.add(channelName)

            if self.show_info == True:
                data = ""
                for channel in debugChannels:
                    data = data + channel + " "
                print("{0}: {1}".format(name, data))
        if "branches" in projectConf:
            branches = projectConf["branches"]
            if "allow" in branches:
                allowBranches = branches["allow"]
            if "deny" in branches:
                denyBranches = branches["deny"]
        if len(allowBranches) == 0 and "*" not in denyBranches:
            allowBranches.append("*")
        elif len(denyBranches) == 0 and "*" not in allowBranches:
            denyBranches.append("*")
        project.actions = actionsDict
        project.channels = channelsDict
        project.options = options
        project.allow_branches = allowBranches
        project.deny_branches = denyBranches
        self.projects[url] = project

    def loadConfiguration(self, name):
        doc = self.load_yaml(name)
        #print doc
        self.loadDefaultOptions()
        if "options" in doc:
            self.global_options.update(doc["options"])
            self.global_options.update_fields()
        projectsList = doc["projects"]
        for project in projectsList:
            if type(project) is str:
                self.loadProjectConfiguration(projectsList[project], project)
            else:
                name = next(iter(project))
                self.loadProjectConfiguration(project[name], name)

